/*
 * Copyright (c) 2005-2007 by Platform Computing Corporation.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Platform Computing, Inc. You shall not disclose such Confidential
 * Information and shall use it only in accordance with the terms of the
 * license agreement you entered into with Platform.
 *
 * Author     ghu
 * Created    Dec 15 , 2009
 * Version    $Id: CommandWrapper.java,v 1.1 2010/02/24 02:32:24 yongdu Exp $
 */

package com.wms.execcmd;

import java.io.IOException;
import java.io.InputStream;

import org.apache.log4j.Logger;


/**
 * The CommandWrapper is used to run a system command
 * 
 */
public final class CommandWrapper implements ITimeoutListener {
	/** Reader to read outputs from stdout */
	private ICommandOutputReader stdoutReader = null;

	/** Reader to read outputs from stderr */
	private ICommandOutputReader stderrReader = null;

	/** Logging handler */
	private Logger logger = null;

	private Process process = null;

	private ThreadGroup readerThreads = null;

	/*
	 * Construct an CommandWrapper object with the given logging handler
	 */
	public CommandWrapper(Logger logger) {
		this.logger = logger;
		this.readerThreads = new ThreadGroup("readerThreads");
	}

	/**
	 * 
	 * @param cmd
	 * @throws IOException
	 */
	public int exec(String[] cmd, String[] envp) throws IOException {
		return this.exec(cmd, envp, -1);
	}

	/**
	 * Execute the given command
	 * 
	 * @param cmd
	 *            command and args to execute
	 * @param timeOut
	 *            time to kill the given process.
	 * @throws IOException
	 */
	public int exec(String[] cmd, String[] envp, long timeOut)
			throws IOException {
		int retValue = -1;

		if (this.logger.isDebugEnabled()) {
			String fullCmd = "Command to execute: ";
			for (String str : cmd) {
				fullCmd += str + " ";
			}
			this.logger.debug(fullCmd);
		}
		this.process = Runtime.getRuntime().exec(cmd, envp);
		InputStream stdout = null;
		InputStream stderr = null;
		Thread stdoutReader = null;
		Thread errorReader = null;

		try {
			// Read outputs from stdout
			if (this.stdoutReader != null) {
				stdout = this.process.getInputStream();
				stdoutReader = new Thread(this.readerThreads, new OutputReader(
						this.stdoutReader, stdout));
				stdoutReader.start();
			}

			// Read outputs from stderr
			if (this.stderrReader != null) {
				stderr = this.process.getErrorStream();
				errorReader = new Thread(this.readerThreads, new OutputReader(
						this.stderrReader, stderr));
				errorReader.start();
			}

			Thread timeCounter = null;
			// Start the time counter thread
			if (timeOut > 0) {
				timeCounter = new Thread(new TimeCounter(this, timeOut));
				timeCounter.start();
			}

			try {
				if (this.process != null) {
					this.process.waitFor();
				}
			}
			catch (InterruptedException ex) {
				// Ignore
			}

			if (this.process != null) {
				retValue = this.process.exitValue();
			}
			else {
				retValue = -1;
			}

			// Wait for the stdout reader
			try {
				stdoutReader.join();
			}
			catch (InterruptedException ex) {
				// Ignore
			}

			// Wait for the stderr reader
			try {
				errorReader.join();
			}
			catch (InterruptedException ex) {
				// Ignore
			}

			// Wake the time couter thread
			if (timeCounter != null) {
				timeCounter.interrupt();
				timeCounter = null;
			}
		}
		finally {
			try {
				if (this.process != null) {
					this.process.destroy();
					this.process = null;
				}
			}
			catch (Exception ex) {
				// Do nothing
			}

			try {
				if (stderr != null) {
					stderr.close();
					stderr = null;
				}
			}
			catch (Exception ex) {
				// Do nothing
			}

			try {
				if (stdout != null) {
					stdout.close();
					stdout = null;
				}
			}
			catch (Exception ex) {
				// Do nothing
			}
		}

		return retValue;
	}

	/**
	 * @param stderrReader
	 *            the stderrReader to set
	 */
	public void setStderrReader(ICommandOutputReader stderrReader) {
		this.stderrReader = stderrReader;
	}

	/**
	 * @param stdoutReader
	 *            the stdoutReader to set
	 */
	public void setStdoutReader(ICommandOutputReader stdoutReader) {
		this.stdoutReader = stdoutReader;
	}

	public void fireTimeout(long timeOut) {
		if (this.process != null) {
			this.process.destroy();
			this.process = null;

			this.readerThreads.interrupt();
			this.notifyAll();
			this.logger
					.warn("The process run out of time and got killed (TimeOut: "
							+ timeOut + " milliseconds).");
		}
	}

	/**
	 * The TimeCounter is used to counter time. If time is out for the given
	 * process, the process will be killed.
	 */
	private class TimeCounter implements Runnable {
		private long timeOut;

		private ITimeoutListener timeoutListener;

		public TimeCounter(ITimeoutListener timeoutListener, long timeOut) {
			this.timeoutListener = timeoutListener;
			this.timeOut = timeOut;
		}

		public void run() {
			try {
				Thread.sleep(this.timeOut);
				this.timeoutListener.fireTimeout(this.timeOut);
			}
			catch (InterruptedException e) {
				// Do nothing
			}
		}
	}

	/**
	 * The OutputReader is used to read stream from the given InputStream object
	 * asynchronously
	 */
	private class OutputReader implements Runnable {
		private ICommandOutputReader reader;

		private InputStream input;

		public OutputReader(ICommandOutputReader reader, InputStream input) {
			this.reader = reader;
			this.input = input;
		}

		public void run() {
			try {
				this.reader.read(this.input);
			}
			catch (IOException ex) {
				logger.error(ex);
			}
		}
	}
}
